package model.fibo;

import java.awt.Color;
import java.awt.Graphics2D;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import model.Model;

/**
 *
 * @author Andrea
 */
public class FiboGame extends Model {

    private final Integer[][] matrix;
    private final Random rnd;
    private volatile int score;
    private static final int MATRIX_START_WIDTH = 4;
    private static final int MATRIX_START_HEIGHT = 4;
    private static final int INITIAL_NUMBER = 2;
    private final Thread daemon;
    private boolean gameStarted;
    
    @SuppressWarnings("CallToThreadStartDuringObjectConstruction")
    public FiboGame() {
        matrix = new Integer[MATRIX_START_WIDTH][MATRIX_START_HEIGHT];

        rnd = new Random();
        
        daemon = new MatrixDaemon();
        daemon.start();
        
        gameStarted = false;
    }

    @Override
    public synchronized void start() {
        gameStarted = true;
        
        score = 0;

        for (Integer[] matrix1 : matrix) {
            for (int j = 0; j < matrix1.length; j++) {
                matrix1[j] = null;
            }
        }

        for (int i = 0; i < INITIAL_NUMBER; i++) {
            insertNumber();
        }

        notifyObservers();
    }

    public synchronized boolean insertNumber() {
        if (isFull()) {
            return false;
        }

        int x;
        int y;

        do {
            x = rnd.nextInt(matrix.length);
            y = rnd.nextInt(matrix[0].length);
        } while (matrix[x][y] != null);

        matrix[x][y] = 1;

        return true;
    }

    @Override
    @SuppressWarnings("NestedSynchronizedStatement")
    public synchronized void moveLeft() {
        boolean isSomethingMoved = false;

        for (int i = 0; i < matrix.length; i++) {
            for (int j = 1; j < matrix[i].length; j++) {
                if (matrix[i][j] != null) {
                    for (int k = 0; k < j; k++) {

                        int y = j;

                        if (matrix[i][k] == null) {
                            isSomethingMoved = true;

                            matrix[i][k] = matrix[i][j];
                            matrix[i][j] = null;

                            if (k > 0 && FibonacciDB.isFibonacciNumber(matrix[i][k], matrix[i][k - 1])) {
                                matrix[i][k - 1] = matrix[i][k - 1] + matrix[i][k];
                                matrix[i][k] = null;

                                score += matrix[i][k - 1].doubleValue();

                                notifyScoreChanged();
                            }

                            break;
                        }

                        if (k == y - 1 && FibonacciDB.isFibonacciNumber(matrix[i][y], matrix[i][y - 1])) {
                            isSomethingMoved = true;

                            matrix[i][y - 1] = matrix[i][y - 1] + matrix[i][y];
                            matrix[i][y] = null;

                            score += matrix[i][y - 1].doubleValue();

                            notifyScoreChanged();

                            break;
                        }
                    }
                }
            }
        }
        if (isSomethingMoved) {
            insertNumber();
        }

        synchronized (daemon) {
            daemon.notify();
        }

        notifyObservers();
    }

    @Override
    @SuppressWarnings("NestedSynchronizedStatement")
    public synchronized void moveRight() {
        boolean isSomethingMoved = false;

        for (int i = 0; i < matrix.length; i++) {
            for (int j = matrix[i].length - 1; j >= 0; j--) {
                if (matrix[i][j] != null) {
                    for (int k = matrix[i].length - 1; k > j; k--) {
                        if (matrix[i][k] == null) {
                            isSomethingMoved = true;

                            matrix[i][k] = matrix[i][j];
                            matrix[i][j] = null;

                            if (k < matrix[i].length - 1 && FibonacciDB.isFibonacciNumber(matrix[i][k], matrix[i][k + 1])) {
                                matrix[i][k + 1] = matrix[i][k + 1] + matrix[i][k];
                                matrix[i][k] = null;

                                score += matrix[i][k + 1].doubleValue();

                                notifyScoreChanged();
                            }

                            break;
                        }

                        if (k == j + 1 && FibonacciDB.isFibonacciNumber(matrix[i][j], matrix[i][k])) {
                            isSomethingMoved = true;

                            matrix[i][k] = matrix[i][k] + matrix[i][j];
                            matrix[i][j] = null;

                            score += matrix[i][k].doubleValue();

                            notifyScoreChanged();

                            break;
                        }
                    }
                }
            }
        }

        if (isSomethingMoved) {
            insertNumber();
        }

        synchronized (daemon) {
            daemon.notify();
        }

        notifyObservers();
    }

    @Override
    @SuppressWarnings("NestedSynchronizedStatement")
    public synchronized void moveUp() {
        boolean isSomethingMoved = false;

        for (int i = 1; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                if (matrix[i][j] != null) {
                    for (int k = 0; k < i; k++) {
                        if (matrix[k][j] == null) {
                            isSomethingMoved = true;

                            matrix[k][j] = matrix[i][j];
                            matrix[i][j] = null;

                            if (k > 0 && FibonacciDB.isFibonacciNumber(matrix[k][j], matrix[k - 1][j])) {
                                matrix[k - 1][j] = matrix[k - 1][j] + matrix[k][j];
                                matrix[k][j] = null;

                                score += matrix[k - 1][j].doubleValue();

                                notifyScoreChanged();
                            }

                            break;
                        }

                        if (k == i - 1 && FibonacciDB.isFibonacciNumber(matrix[i][j], matrix[k][j])) {
                            isSomethingMoved = true;

                            matrix[k][j] = matrix[k][j] + matrix[i][j];
                            matrix[i][j] = null;

                            score += matrix[k][j].doubleValue();

                            notifyScoreChanged();

                            break;
                        }
                    }
                }
            }
        }

        if (isSomethingMoved) {
            insertNumber();
        }

        synchronized (daemon) {
            daemon.notify();
        }

        notifyObservers();
    }

    @Override
    @SuppressWarnings("NestedSynchronizedStatement")
    public synchronized void moveDown() {
        boolean isSomethingMoved = false;

        for (int i = matrix.length - 2; i >= 0; i--) {

            for (int j = 0; j < matrix[i].length; j++) {

                if (matrix[i][j] != null) {

                    for (int k = matrix.length - 1; k > i; k--) {

                        if (matrix[k][j] == null) {
                            isSomethingMoved = true;

                            matrix[k][j] = matrix[i][j];
                            matrix[i][j] = null;

                            if (k < matrix.length - 1 && FibonacciDB.isFibonacciNumber(matrix[k][j], matrix[k + 1][j])) {
                                matrix[k + 1][j] = matrix[k + 1][j] + matrix[k][j];
                                matrix[k][j] = null;

                                score += matrix[k + 1][j].doubleValue();

                                notifyScoreChanged();
                            }

                            break;
                        }

                        if (k == i + 1 && FibonacciDB.isFibonacciNumber(matrix[i][j], matrix[k][j])) {
                            isSomethingMoved = true;

                            matrix[k][j] = matrix[k][j] + matrix[i][j];
                            matrix[i][j] = null;

                            score += matrix[k][j].doubleValue();

                            notifyScoreChanged();

                            break;
                        }
                    }
                }
            }
        }

        if (isSomethingMoved) {
            insertNumber();
        }

        synchronized (daemon) {
            daemon.notify();
        }

        notifyObservers();
    }

    private synchronized boolean isFull() {
        for (Integer[] row : matrix) {
            for (Integer cell : row) {
                if (cell == null) {
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    public int getScore() {
        return score;
    }

    private void debugMatrix() {
        for (Integer[] row : matrix) {
            for (Integer cell : row) {
                System.out.print((cell != null ? cell : "\\") + " ");
            }
            System.out.println();
        }
        System.out.println();
    }

    @Override
    public Object[][] getDrawingMatrix() {
        return matrix;
    }

    @Override
    public void draw(Graphics2D g2d) {
        if (!gameStarted) {
            g2d.setColor(Color.LIGHT_GRAY);
            g2d.drawString("fibo game fatto dal Gila",
                    (int) g2d.getClipBounds().getWidth() / 2 - g2d.getFontMetrics().stringWidth("2048 rifatto dal gila") / 2,
                    (int) g2d.getClipBounds().getHeight() / 2);
            return;
        }

        g2d.setColor(Color.white);
        g2d.fill3DRect(0, 0, (int) g2d.getClipBounds().getWidth(), (int) g2d.getClipBounds().getHeight(), true);

        int cellWidth = (int) (g2d.getClipBounds().getWidth() / matrix[0].length);
        int cellHeight = (int) (g2d.getClipBounds().getHeight() / matrix.length);

        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {

                g2d.setColor(Color.DARK_GRAY);
                g2d.draw3DRect(j * cellWidth, i * cellHeight, cellWidth, cellHeight, true);

                if (matrix[i][j] != null) {
                    
                    g2d.setColor(new Color((matrix[i][j]*255/100) % 256, (matrix[i][j]*255/44 + 134) % 256, (matrix[i][j]*255/66 + 123) % 256));
                    g2d.fill3DRect(j * cellWidth, i * cellHeight, cellWidth, cellHeight, true);
                    
                    g2d.setColor(Color.white);
                    g2d.drawString(matrix[i][j].toString(),
                            j * cellWidth + cellWidth / 2 - g2d.getFontMetrics().stringWidth(matrix[i][j].toString()) / 2,
                            i * cellHeight + cellHeight / 2);
                }else{
                    g2d.setColor(Color.LIGHT_GRAY);
                    g2d.fill3DRect(j * cellWidth, i * cellHeight, cellWidth, cellHeight, true);
                }
            }
        }
    }

    @Override
    public void setColor(Color c) {
    }

    private class MatrixDaemon extends Thread {

        public MatrixDaemon() {
            super("matrix-daemon");

            this.setDaemon(true);
        }

        @Override
        public void run() {
            while (true) {

                if (isFull()) {

                    boolean gameOver = true;
                    long start = System.nanoTime();

                    for (int i = 0; i < matrix.length * matrix[0].length; i += 2) {
                        int y = i / matrix.length;
                        int x = i % matrix.length + (y % 2 == 0 ? 0 : 1);

                        if (x < matrix.length - 1 && FibonacciDB.isFibonacciNumber(matrix[x][y], matrix[x + 1][y])) {
                            gameOver = false;

                            break;
                        } else if (x > 1 && FibonacciDB.isFibonacciNumber(matrix[x][y], matrix[x - 1][y])) {
                            gameOver = false;

                            break;
                        } else if (y < matrix[x].length - 1 && FibonacciDB.isFibonacciNumber(matrix[x][y], matrix[x][y + 1])) {
                            gameOver = false;

                            break;
                        } else if (y > 0 && FibonacciDB.isFibonacciNumber(matrix[x][y], matrix[x][y - 1])) {
                            gameOver = false;

                            break;
                        }
                    }

                    if (gameOver) {
                        notifyGameOver();
                        
                        System.gc();
                    }
                }

                synchronized (this) {
                    try {
                        this.wait();
                    } catch (InterruptedException ex) {
                        Logger.getLogger(Model.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        }
    }
}
